package de.zainodis.commons.utils;

import java.io.StringReader;
import java.io.StringWriter;
import java.util.Hashtable;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.helpers.DefaultHandler;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;

import de.zainodis.commons.LogCat;
import de.zainodis.commons.exception.SerializationException;
import de.zainodis.commons.exception.XmlParseException;

/**
 * Offers utility methods for DOM-based and Pull-based parser alike.
 * 
 * @author fzarrai
 * 
 */
public final class XmlUtils {

   private static final String TAG = "XmlUtils";

   /**
    * Creates an XML {@link Document} from the given {@link InputSource}. This
    * requires the InputSource to contain valid XML.
    * 
    * @param source
    *           InputSource from which the XML Document will be created
    * @return the Document that has been created from the provided InputSource
    * @throws XmlParseException
    *            if an error occurs during the creation of the Document from the
    *            given InputSource
    */
   public static Document createXmlDocument(InputSource source) throws XmlParseException {

	 try {
	    DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
	    DocumentBuilder builder = domFactory.newDocumentBuilder();

	    return builder.parse(source);

	 } catch (Exception e) {
	    // Wrap and rethrow
	    throw new XmlParseException(e.getMessage(), e);
	 }
   }

   /**
    * Wraps the given String in enclosing tags "<" and ">" and returns the
    * result. Example: If the string "hello World" is passed the return value of
    * the method would look as follows: <hello World>.
    */
   public static String tag(String value) {
	 return String.format("<%s>", value);
   }

   /**
    * Validates a given XML.
    * 
    * @param xml
    *           the xml string to validate.
    * @return true if the xml is valid; false otherwise.
    */
   public static boolean isValidXml(String xml) {
	 if (xml != null) {
	    SAXParserFactory factory = SAXParserFactory.newInstance();
	    try {
		  // Wrap the input string in an input source
		  InputSource inputSource = new InputSource();
		  inputSource.setCharacterStream(new java.io.StringReader(xml));
		  SAXParser parser = factory.newSAXParser();
		  // Pass the wrapped string to the sax parser. If no exception
		  // occurs, the xml is valid
		  parser.parse(inputSource, new DefaultHandler());
		  return true;
	    } catch (Exception e) {
		  return false;
	    }
	 }
	 return false;

   }

   /**
    * Parses all attributes of the element to which the {@link XmlPullParser}
    * currently points. The attributes are stored in a map. The attribute name
    * serves as the key and the attribute value as the value. If the current
    * element has no attributes, an empty map is returned.
    * 
    * @param parser
    *           an {@link XmlPullParser} which must be pointing to an element
    *           whose attributes will be parsed.
    * @return a map of attributes which matches the attribute names to their
    *         values.
    * @throws XmlParseException
    *            if the element to which the parser currently points has no
    *            attributes.
    */
   public static Map<String, String> parseAttributes(XmlPullParser parser) {
	 int attributeCount = parser.getAttributeCount();

	 Map<String, String> attrs = new Hashtable<String, String>();

	 if (attributeCount != -1) {
	    for (int x = 0; x < attributeCount; x++) {
		  String attributeName = parser.getAttributeName(x);
		  String attributeValue = parser.getAttributeValue(x);

		  attrs.put(attributeName, attributeValue);
	    }
	 }
	 return attrs;
   }

   /**
    * Parses all attributes from the given {@link Node} element and converts the
    * resulting {@link NamedNodeMap} to a {@link Map}.
    * 
    * @param element
    *           the node from which the attributes are being read.
    * @return a map of attributes which matches the attribute names to their
    *         values.
    */
   public static Map<String, String> parseAttributes(Node element) {
	 Map<String, String> result = new Hashtable<String, String>();
	 NamedNodeMap attributes = element.getAttributes();

	 for (int index = 0; index < attributes.getLength(); index++) {
	    Attr attribute = (Attr) attributes.item(index);
	    result.put(attribute.getName(), attribute.getValue());
	 }
	 return result;
   }

   /**
    * Attempts to read the attribute with the given name from the given
    * {@link Node} element and returns it's value.
    * 
    * @param name
    *           the name of the attribute
    * @return the value of the read attribute; or null if the attribute was not
    *         found or it's value is null, or the given {@link Node} is null.
    */
   public static String readAttribute(Node element, String name) {
	 if (element != null) {
	    Map<String, String> attributes = parseAttributes(element);
	    return attributes.get(name);
	 }
	 return null;
   }

   /**
    * Attempts to read the attribute with the given name from the current
    * element of the given {@link XmlPullParser} and returns it's value.
    * 
    * @param name
    *           the name of the attribute
    * @return the value of the read attribute; or null if the attribute was not
    *         found or it's value is null.
    */
   public static String readAttribute(XmlPullParser parser, String name) {
	 Map<String, String> attributes = parseAttributes(parser);
	 return attributes.get(name);
   }

   /**
    * Writes the child nodes of the given parent node to xml and returns the
    * result.
    * 
    * @param parent
    *           the parent {@link Node}.
    * @return the children, written to xml.
    */
   public static String getChildrenAsXml(Node parent) {
	 StringBuilder resultBuilder = new StringBuilder();
	 // Get all children
	 NodeList children = parent.getChildNodes();
	 try {

	    // Set up the output transformer
	    TransformerFactory transfac = TransformerFactory.newInstance();
	    Transformer trans = transfac.newTransformer();
	    trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
	    trans.setOutputProperty(OutputKeys.INDENT, "yes");
	    StringWriter stringWriter = new StringWriter();
	    StreamResult streamResult = new StreamResult(stringWriter);

	    for (int index = 0; index < children.getLength(); index++) {
		  Node child = children.item(index);

		  // Print the DOM node
		  DOMSource source = new DOMSource(child);
		  trans.transform(source, streamResult);
		  // Append child to end result
		  resultBuilder.append(stringWriter.toString());
	    }
	 } catch (TransformerException e) {
	    LogCat.e(TAG, "Failed to transform node content to xml.");
	 }
	 return resultBuilder.toString();
   }

   /**
    * Determines, if an {@link XmlPullParser} event is an end tag.
    * 
    * @param eventType
    *           the event type of an XmlPullParser.
    * @return true if the provided event type is an end element; false
    *         otherwise.
    */
   public static boolean isEndTag(int eventType) {
	 return eventType == XmlPullParser.END_TAG;
   }

   /**
    * Determines, if an {@link XmlPullParser} event is an end tag and if the
    * name of that end tag matches the given, expected name.
    * 
    * @param eventType
    *           the event type of an XmlPullParser.
    * @return true if the provided event type is an end element and its name
    *         matches the expected name; false otherwise.
    */
   public static boolean isEndTag(XmlPullParser parser, String expectedName) {
	 try {
	    boolean isEndTag = isEndTag(parser.getEventType());
	    boolean isExpectedTag = expectedName.equals(parser.getName());
	    return isEndTag && isExpectedTag;

	 } catch (XmlPullParserException e) {
	    String errorMessage = String.format(
			"Failed to determines event type. Expected tag [%s], actual tag [%s].",
			expectedName, parser.getName());
	    LogCat.e(TAG, errorMessage);
	    throw new SerializationException(errorMessage, e);
	 }
   }

   /**
    * Determines, if an {@link XmlPullParser} event is a start tag.
    * 
    * @param eventType
    *           the event type of an XmlPullParser.
    * @return true if the provided event type is a start element; false
    *         otherwise.
    */
   public static boolean isStartTag(int eventType) {
	 return eventType == XmlPullParser.START_TAG;
   }

   /**
    * Determines, if an {@link XmlPullParser} event is a text element.
    * 
    * @param eventType
    *           the event type of an XmlPullParser.
    * @return true if the provided event type is a text element; false
    *         otherwise.
    */
   public static boolean isTextElement(int eventType) {
	 return eventType == XmlPullParser.TEXT;
   }

   /**
    * Determines, if the current element of an {@link XmlPullParser} is an empty
    * tag. Empty tags may contain attributes, but no further elements.
    * 
    * @return true if the current element is an empty element; false otherwise.
    */
   public static boolean isEmptyElement(XmlPullParser parser) {
	 try {
	    return parser.isEmptyElementTag();
	 } catch (XmlPullParserException e) {
	    String errorMessage = "Failed to check if " + parser.getName()
			+ " is an empty element tag.";
	    LogCat.e(TAG, errorMessage);
	    // Cant be determined so its not an empty element or an element at
	    // all
	    return false;
	 }
   }

   /**
    * Determines, if an {@link XmlPullParser} event marks the beginning of an
    * xml document.
    * 
    * @param eventType
    *           the event type of an XmlPullParser.
    * @return true if the provided event type marks the beginning of an xml
    *         document; false otherwise.
    */
   public static boolean isDocumentStart(int eventType) {
	 return eventType == XmlPullParser.START_DOCUMENT;
   }

   /**
    * Determines, if an {@link XmlPullParser} event marks the end of an xml
    * document.
    * 
    * @param eventType
    *           the event type of an XmlPullParser.
    * @return true if the provided event type marks the end of an xml document;
    *         false otherwise.
    */
   public static boolean isDocumentEnd(int eventType) {
	 return eventType == XmlPullParser.END_DOCUMENT;
   }

   /**
    * Determines if an {@link XmlPullParser} event is a document declaration.
    * 
    * @param eventType
    *           the event type of an XmlPullParser.
    * @return true if the provided event type is a document declaration.
    */
   public static boolean isDocumentDeclaration(int eventType) {
	 return eventType == XmlPullParser.DOCDECL;
   }

   /**
    * Returns true if the given {@link Node} is an element (tag); false
    * otherwise.
    */
   public static boolean isElementNode(Node node) {
	 return node != null && node.getNodeType() == Node.ELEMENT_NODE;
   }

   /**
    * Return true if the given {@link Node} is a text node; false otherwise.
    */
   public static boolean isTextNode(Node node) {
	 return node != null && node.getNodeType() == Node.TEXT_NODE;
   }

   public static XmlPullParser createPullParser(String xml) {

	 try {
	    XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
	    factory.setNamespaceAware(true);
	    XmlPullParser parser = factory.newPullParser();
	    parser.setInput(new StringReader(xml));
	    return parser;
	 } catch (XmlPullParserException e) {
	    String errorMessage = "Failed to create pullparser, cause: " + e.getMessage();
	    LogCat.e(TAG, errorMessage);
	    throw new SerializationException(errorMessage, e);
	 }
   }
}
